在 Ruby 版的震盪之後,Go 版也迎來一次「下巴掉下來」的時刻:
數字殘酷,但結論清楚:瓶頸不在演算法,而在 Go ↔ C 邊界的呼叫成本。本篇把觀測方法、原因拆解、與改善方向攤開來說
基準程式(節錄):
size := 100_000
loops := 5
records := genRecords(size)
expectedSimple := countSimpleQuery(records)
expectedComplex := countComplexQuery(records)
bench("Simple query (Plain Go)", loops, func() { _ = countSimpleQuery(records) })
matcherSimple, _ := mongory.NewCMatcher(map[string]any{
"age": map[string]any{"$gte": 18},
}, nil)
bench("Simple query (Mongory Matcher)", loops, func() {
cnt := 0
for i := range records { ok, _ := matcherSimple.Match(records[i]); if ok { cnt++ } }
if cnt != expectedSimple { panic("count mismatch") }
})
bench("Complex query (Plain Go)", loops, func() { _ = countComplexQuery(records) })
matcherComplex, _ := mongory.NewCMatcher(map[string]any{
"$or": []any{ map[string]any{"age": map[string]any{"$gte": 18}}, map[string]any{"status": "active"} },
}, nil)
bench("Complex query (Mongory Matcher)", loops, func() {
cnt := 0
for i := range records { ok, _ := matcherComplex.Match(records[i]); if ok { cnt++ } }
if cnt != expectedComplex { panic("count mismatch") }
})
量測原則:
debug.SetGCPercent(-1)
,迴圈前後讀取 runtime.MemStats
觀察趨勢結論:當條件與資料結構足夠簡單時,純 Go 直敲記憶體是壓倒性優勢,任何跨界呼叫都會被放大
這些實驗能將「反射成本」與「cgo 固定成本」切開,驗證主要瓶頸確實在跨界次數
筆者第一次看到 2ms vs 90ms 的差距時,確實失望,但更重要的是它給了清晰的方向。與其在邊界上糾結,不如承認「Go 有它的最短路」:把核心改寫成 native Go,才是穩健的長期選擇。C Core 仍然會是 Ruby 與其他語言橋接的關鍵,但在 Go,筆者會押注 native 路線
這次的 shock 再次提醒:效能是系統性的。Ruby 版靠 shallow O(1) 與 pool reset 贏回來,Go 版的贏法,則是直接不使用 cgo 橋接 Mongory-core,應在保留既有 DSL/AST 的前提下,直接使用 native Golang 重寫。